简单的 Webpack Plugin 编写

前言

前端技术发展迅速,各种框架工具库层出不穷,诸如'大前端'、'前后端分离'、'本地开发构建'等等热门词汇我们更是屡见不鲜,这些现代前端技术基石自然离不开 webpack ,webpack 的工作更离不开各类插件 Plugin 的支持

相信不少小伙伴在平时的项目开发中使用过 Webpack Plugins, 我的上篇文章 Webpack4 和 Babel 7 全套配置也有介绍搭配 Webpack Plugins 的配置过程,但对于自己开坑写 webpack ,可能是比较少的(包括我寄几),这两天抽空看了下 webpack 中文文档(v4.15.1),又逢读到 Webpack 原理-编写 Plugin 好文章一篇,豁然开朗,所以简单的介绍下插件开发的过程

例子中使用的插件我已经发到 npm 用来测试了

基本插件结构

webpack 插件是一个具有 apply 属性的 JavaScript 对象。apply 属性会被 webpack compiler 调用,并且 compiler 对象可在整个编译生命周期访问

class BasicPlugin{
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options){
  }
  
  // Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler){
    compiler.hooks.run.tap(pluginName, compilation => {
      // console.log("webpack 构建过程开始!")
    })
  }
}

// 导出 Plugin
module.exports = BasicPlugin

compiler hooktap 方法的第一个参数,应该是驼峰式命名的插件名称。建议为此使用一个常量,以便它可以在所有 hook 中复用

配置

const BasicPlugin = require('BasicPlugin.js')
module.export = {
  plugins:[
    new BasicPlugin(options),
  ]
}
  • Webpack 启动后,在读取配置的过程中会先执行 new BasicPlugin(options) 初始化一个 BasicPlugin 获得其实例
  • 在初始化 compiler 对象后,再调用 basicPlugin.apply(compiler) 给插件实例传入 compiler 对象
  • 插件实例在获取到 compiler 对象后,就可以通过 compiler hooktap 方法监听到 Webpack 广播出来的事件,hook 上绑定了事件名称,第二个参数是回调函数

Compiler 和 Compilation

在基本结构中比较重要的两个对象就是 CompilerCompilation ,它们是 PluginWebpack 之间的桥梁

Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例

Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象

Compiler 和 Compilation 的区别在于

Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译

事件流

webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果

插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理

Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作

Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好,事件流机制应用了观察者模式

/**
* 广播出事件
* event-name 为事件名称,注意不要和现有的事件重名
* params 为附带的参数
*/
compiler.apply('event-name',params)

/**
* 监听名称为 event-name 的事件,当 event-name 事件发生时,函数就会被执行。
* 同时函数中的 params 参数为广播事件时附带的参数。
*/
compiler.hooks.event-name.tap('plugin-name',function(params) {
  
})

apply 方法是必须要有的,因为当我们使用一个插件时( new somePlugins({})),webpack 会去寻找插件的 apply 方法并执行

compiler.hooks.event-name.tap() 就相当于给 compiler 设置了事件监听,当监听到 event-name (Compilation)事件,应该做些相应操作,类似于 document.addEventListener

实战

下面写一个插件名为 LogWebpackPlugin 的例子,作用是 Webpack 在执行过程中打印一些日志,实现该插件非常简单,完整代码如下

class LogWebpackPlugin {

  constructor(doneCallback, emitCallback) {
    // 存下在构造函数中传入的回调函数
    this.emitCallback = emitCallback
    this.doneCallback = doneCallback
  }

  apply(compiler) {
    compiler.hooks.emit.tap('LogWebpackPlugin', () => {
      // 在 emit 事件中回调 emitCallback
      this.emitCallback()
    })
    compiler.hooks.done.tap('LogWebpackPlugin', () => {
      // 在 done 事件中回调 doneCallback
      this.doneCallback()
    })
    compiler.hooks.compilation.tap('LogWebpackPlugin', () => {
      // compilation('编译器'对'编译ing'这个事件的监听)
      console.log("The compiler is starting a new compilation...")
    })
    compiler.hooks.compile.tap('LogWebpackPlugin', () => {
      // compile('编译器'对'开始编译'这个事件的监听)
      console.log("The compiler is starting to compile...")
    })
  }
}

// 导出插件 
module.exports = LogWebpackPlugin

使用该插件的方法如下

npm i log-webpack-plugin --D
const logWebpackPlugin = require('log-webpack-plugin') 

module.exports = {
  plugins: [
    // 在初始化 logWebpackPlugin 时传入了两个参数,分别是在成功时的回调函数和模块完成转换的回调函数;
    new logWebpackPlugin(() => {
      // Webpack 模块完成转换成功
      console.log('emit 事件发生啦,所有模块的转换和代码块对应的文件已经生成好~')
    } , () => {
      // Webpack 构建成功,并且文件输出了后会执行到这里,在这里可以做发布文件操作
      console.log('done 事件发生啦,成功构建完成~')
    })
  ]
}

npm run build 编译结果

image

完整插件可见 log-webpack-plugin